package com.gotobject.junit.internal.runners.statements;

import com.gotobject.junit.ConfigureWith;
import com.gotobject.junit.internal.runners.model.PerformanceKey;
import com.gotobject.junit.internal.runners.model.Explosion;
import org.junit.internal.runners.statements.FailOnTimeout;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author Huascar A. Sanchez (huascar.sanchez@gmail.com)
 */
public class FailOnExplosionShell {
    private final Class<?>          source;
    private final AtomicBoolean     manualExplosion;
    private final List<Explosion>   explosions;
    private final Builder           builder;

    private static final int LOWER_LIMIT = 2;
    private static final int UPPER_LIMIT = 3;

    private FailOnExplosionShell(Builder builder){
        this.builder            = builder;
             source             = builder.testClass.getJavaClass();
             manualExplosion    = new AtomicBoolean(builder.explosions.isEmpty());
             explosions         = builder.explosions;

        tryCollectingExplosions();
    }

    public static <T extends Annotation> T annotationFor(FrameworkMethod method, Class<? extends T> klass){
		return method.getAnnotation(klass);
	}

    public static void fireRuntimeException(String msg) throws RuntimeException {
        throw new RuntimeException(msg);
    }

    private void tryCollectingExplosions() {
        if(!explosions.isEmpty()){
            return;
        }

        try {
            for(FrameworkMethod each : builder.testClass.getAnnotatedMethods(ConfigureWith.class)){
                final ConfigureWith data 	= annotationFor(each, ConfigureWith.class);
                if(each.getName().equals(builder.configuredMethod) || data.value().equals(builder.configuredMethod)){
                    final Object[][] 	found 	= Object[][].class.cast(each.invokeExplosively(null));
                    for(Object[] eachArray : found){
                        final int length = eachArray.length;
                        if(length < LOWER_LIMIT && length > UPPER_LIMIT){
                            fireRuntimeException(String.format(" wrong number of parameters: %d", eachArray.length));
                        }

                        final int  users 	= Integer.class.cast(eachArray[0]);
                        final int  iter  	= Integer.class.cast(eachArray[1]);
                        // very important factor:
                        // if the user decide to provide a response time value in the @ConfigureWith method - we are cool
                        // as long as the user does not provide this value again in the @Test method (i.e., @Test(timeout=????)
                        // if the user does that the latter of the timeout values will be used as the desired one; it will override
                        // the first one.
                        // Load testing is agnostic to this since this test does not take into consideration the timeout value.
                        final long resp     = Long.class.cast(length == 2
                                ? builder.timeout : builder.timeout > 0L
                                ? builder.timeout : eachArray[2]);

                        explosions.add(new Explosion(users, iter, resp));

                    }
                }
            }

            if(explosions.isEmpty()){
                explosions.add(new Explosion(1, 1, 10000000L));
            }

        } catch (Throwable e){
            //noinspection ThrowableInstanceNeverThrown
            final Exception internalEx = new Exception("Internal error when extracting load configuration.", e);
            if(builder.notifier != null){
                builder.notifier.fireTestFailure(new Failure(builder.description, internalEx));
            }
        }
    }

    public Statement newStatement(){
        final Statement             next            = builder.statement;
        final PerformanceKey        toLiteral       = builder.perfType;
        final String   	    	    nameOfConfig    = toLiteral.getConfigName();
        final Description		    desc			= builder.description;
        final long				    timeout			= builder.timeout;
        final boolean               inAutocruise    = !manualExplosion.get();
        final List<Explosion> 	    input			= inAutocruise && !"<n/a>".equals(nameOfConfig)
                                                        ? Arrays.asList(explosions.get(0))
                                                        : new ArrayList<Explosion>(explosions);
        final Statement result;
        switch(toLiteral){
            case LOAD:
                result = new FailOnLoadExplosion(next, input, desc);
                break;
            case RESPONSE:
                final FailOnTimeout timedTest = new FailOnTimeout(next, timeout);
                result = new FailOnResponseTimeoutExplosion(timedTest, input, desc);
                break;
            case THROUGHPUT:
                final FailOnLoadExplosion loadTest = new FailOnLoadExplosion(next, input, desc);
                result = new FailOnTimeout(loadTest, timeout);
                break;
            case UNKNOWN:
                result = next;
                break;
            default:
                throw new IllegalStateException("An unknown test literal has been used.");
        }

        return result;
    }

    /**
     * @author Huascar A. Sanchez (huascar.sanchez@gmail.com)
     */
    public static class Builder {
        private final   List<Explosion>     explosions;
        private         TestClass           testClass;
        private         Description         description;
        private         RunNotifier         notifier;
        private         String              configuredMethod;
        private         Statement           statement;
        private 		PerformanceKey 		perfType;
        private         long                timeout;

        public Builder(List<Explosion> explosions){
            this.explosions 	= explosions;
			testClass			= null;
			description			= null;
			notifier			= null;
			configuredMethod 	= null;
			statement			= null;
			perfType			= null;
			timeout				= 0L;
		}

        public FailOnExplosionShell build(){
            if(!explosions.isEmpty()){
                // this only applied if we are in autocruised...or tests
                // were defined in a xml file.
                timeout(explosions.get(0).getResponseTime());
            }

            return new FailOnExplosionShell(this);
        }

        public Builder configuredMethod(String label){
            configuredMethod = label;
            return this;
        }

        public Builder description(Description description){
            this.description = description;
            return this;
        }

        public Builder next(Statement statement){
            this.statement = statement;
            return this;
        }

        public Builder notifier(RunNotifier notifier){
            this.notifier = notifier;
            return this;
        }

        public Builder perfType(PerformanceKey type){
            perfType = type;
            return this;
        }

        public Builder root(TestClass testClass){
            this.testClass = testClass;
            return this;
        }

        public Builder timeout(long value){
            timeout = value;
            return this;
        }
    }

    @Override
    public String toString() {
        return source.getName() + " with " + explosions;
    }
}
